🏗️ Le Strutture in C

Guida Completa ed Esaustiva ai Tipi di Dato Aggregati Definiti dall'Utente

📚 Introduzione: La Necessità di Aggregare Dati Eterogenei

Immaginiamo di dover gestire i dati di uno studente universitario. Ogni studente ha molteplici informazioni associate: un nome (una stringa di caratteri), una matricola (un numero intero), una media dei voti (un numero decimale), un anno di iscrizione (un intero), un indirizzo email (una stringa), e così via. Se dovessimo gestire queste informazioni con le strutture dati viste finora, ci troveremmo di fronte a diverse possibilità, tutte con limiti significativi.

Potremmo utilizzare variabili separate per ogni dato: char nome[50], int matricola, float media, int anno, char email[100]. Questa soluzione però diventa rapidamente ingestibile quando dobbiamo gestire più studenti. Dovremmo dichiarare nome1[50], nome2[50], nome3[50]... per ogni studente, moltiplicando le variabili in modo esponenziale.

Potremmo pensare di usare array paralleli: char nomi[100][50], int matricole[100], float medie[100]. Questa soluzione funziona, ma ha un problema fondamentale: non c'è un legame esplicito tra i dati. L'informazione che nomi[5], matricole[5] e medie[5] si riferiscono allo stesso studente è solo implicita nella nostra gestione del codice. Se per errore scordiamo di aggiornare uno degli array, o se li ordiniamo in modo diverso, perdiamo completamente la coerenza dei dati.

È qui che entrano in gioco le strutture (in inglese struct), uno dei concetti più potenti e fondamentali del linguaggio C. Le strutture ci permettono di creare nuovi tipi di dato aggregati, che raggruppano logicamente variabili di tipo diverso sotto un'unica entità. Possiamo pensare alle strutture come a dei contenitori personalizzati che definiscono un nuovo "tipo" di dato complesso, perfettamente adatto a rappresentare entità del mondo reale come studenti, conti bancari, coordinate geometriche, date, documenti e qualsiasi altra entità che richieda più informazioni eterogenee correlate tra loro.

🎯 Concetto Fondamentale: Cosa Sono le Strutture

Una struttura in C è un tipo di dato derivato che consente di raggruppare variabili di tipo diverso (o anche dello stesso tipo) in un'unica unità logica. A differenza degli array, che possono contenere solo elementi dello stesso tipo, le strutture permettono di combinare dati eterogenei. Ogni variabile all'interno di una struttura è chiamata campo o membro della struttura.

💡 Differenza Fondamentale: Array vs Strutture

Gli array sono collezioni omogenee di elementi dello stesso tipo, accessibili tramite un indice numerico. Ad esempio, int voti[10] contiene 10 valori interi indicizzati da 0 a 9.

Le strutture sono collezioni eterogenee di elementi potenzialmente di tipo diverso, accessibili tramite nome. Ad esempio, una struttura Studente può contenere un nome (stringa), una matricola (intero) e una media (float), ciascuno accessibile con il proprio nome specifico.

Le strutture sono fondamentali per implementare l'astrazione dei dati, uno dei pilastri della programmazione moderna. Ci permettono di creare rappresentazioni software di entità del mondo reale, raggruppando tutte le informazioni rilevanti in un'unica unità coerente. Questo approccio migliora notevolmente la leggibilità, la manutenibilità e l'organizzazione del codice.

📝 Dichiarazione di una Struttura

La dichiarazione di una struttura definisce un nuovo tipo di dato, specificando quali campi (membri) compongono la struttura e il tipo di ciascun campo. La dichiarazione non alloca memoria: essa semplicemente comunica al compilatore come sarà organizzata la struttura quando verrà effettivamente creata una variabile di quel tipo.

🔧 Sintassi Base di Dichiarazione

La sintassi per dichiarare una struttura in C utilizza la parola chiave struct seguita da un nome identificativo (chiamato tag) e da un blocco racchiuso tra parentesi graffe che contiene le dichiarazioni dei membri:

struct nome_struttura {
    tipo1 membro1;
    tipo2 membro2;
    tipo3 membro3;
    // ... altri membri
};
⚠️ Attenzione al Punto e Virgola

È fondamentale ricordare di inserire il punto e virgola alla fine della dichiarazione della struttura, dopo la parentesi graffa chiusa. Questo è un errore comune che può generare messaggi di errore del compilatore difficili da interpretare. Il punto e virgola è necessario perché la dichiarazione di una struttura è considerata una istruzione completa in C.

📌 Esempio Pratico: Struttura Studente

Creiamo una struttura per rappresentare uno studente universitario con le informazioni essenziali:

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    float media;
    int anno_iscrizione;
};

In questo esempio, abbiamo definito una struttura chiamata Studente che contiene cinque membri: due array di caratteri per il nome e cognome (ciascuno di 50 caratteri), un intero per la matricola, un float per la media dei voti, e un intero per l'anno di iscrizione. Questa dichiarazione crea un nuovo tipo di dato che può essere utilizzato in tutto il programma.

📌 Esempio Pratico: Struttura Punto2D

Un altro esempio comune è la rappresentazione di un punto in uno spazio bidimensionale:

struct Punto2D {
    float x;
    float y;
};

Questa semplice struttura contiene solo due membri di tipo float, rappresentando le coordinate x e y di un punto sul piano cartesiano. Nonostante la semplicità, questa struttura è estremamente utile in applicazioni grafiche, giochi, simulazioni fisiche e calcoli geometrici.

📌 Esempio Pratico: Struttura Data

Rappresentiamo una data del calendario:

struct Data {
    int giorno;
    int mese;
    int anno;
};

La struttura Data raggruppa tre informazioni correlate (giorno, mese, anno) in un'unica entità logica. Questo rende molto più semplice passare date come parametri alle funzioni, confrontare date, o organizzare array di date.

🔄 Dichiarazione e Definizione Simultanea di Variabili

È possibile dichiarare la struttura e contemporaneamente definire una o più variabili di quel tipo, specificando i nomi delle variabili dopo la parentesi graffa chiusa ma prima del punto e virgola:

struct Libro {
    char titolo[100];
    char autore[50];
    int anno_pubblicazione;
    float prezzo;
} libro1, libro2, libro3;

In questo esempio, stiamo dichiarando la struttura Libro e creando contestualmente tre variabili (libro1, libro2, libro3) di tipo struct Libro. Questa sintassi è particolarmente utile quando sappiamo già di aver bisogno di alcune variabili di quel tipo.

💡 Posizionamento delle Dichiarazioni

Le dichiarazioni di strutture vengono solitamente posizionate all'inizio del file, prima della funzione main(), oppure in file header dedicati (con estensione .h). Questo permette a tutte le funzioni del programma di utilizzare quel tipo di struttura. Le dichiarazioni globali delle strutture hanno visibilità in tutto il file.

🏷️ Definizione di Variabili di Tipo Struttura

Dopo aver dichiarato una struttura, possiamo definire variabili di quel tipo. La definizione di una variabile struttura alloca effettivamente la memoria necessaria per contenere tutti i membri della struttura. Esistono diversi modi per definire e inizializzare variabili struttura.

🔹 Definizione Base

La sintassi base per definire una variabile struttura richiede la parola chiave struct, il nome della struttura (tag), e il nome della variabile:

struct nome_struttura nome_variabile;

📌 Esempio di Definizione

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    float media;
    int anno_iscrizione;
};

int main() {
    struct Studente studente1;
    struct Studente studente2;
    struct Studente studente3;
    
    return 0;
}

In questo esempio, abbiamo creato tre variabili di tipo struct Studente. Ogni variabile occupa in memoria lo spazio necessario per contenere tutti i cinque membri della struttura. Tuttavia, i valori dei membri non sono inizializzati e contengono valori casuali (garbage) finché non vengono esplicitamente assegnati.

⚠️ Variabili Non Inizializzate

Quando definiamo una variabile struttura senza inizializzarla esplicitamente, i suoi membri contengono valori indeterminati (garbage). È fondamentale inizializzare sempre le variabili prima di utilizzarle, per evitare comportamenti imprevedibili del programma. Questo vale sia per i tipi semplici (int, float) che per gli array di caratteri contenuti nella struttura.

🔹 Inizializzazione durante la Definizione

È possibile inizializzare i membri di una struttura al momento della sua definizione utilizzando una lista di valori racchiusa tra parentesi graffe. I valori devono essere forniti nello stesso ordine in cui sono stati dichiarati i membri nella struttura.

struct nome_struttura nome_variabile = {valore1, valore2, valore3, ...};

📌 Esempio di Inizializzazione Completa

struct Data {
    int giorno;
    int mese;
    int anno;
};

int main() {
    struct Data nascita = {15, 3, 2000};
    
    // La variabile nascita ora contiene:
    // giorno = 15
    // mese = 3
    // anno = 2000
    
    return 0;
}

I valori vengono assegnati nell'ordine di dichiarazione dei membri: il primo valore (15) va al primo membro (giorno), il secondo valore (3) va al secondo membro (mese), e il terzo valore (2000) va al terzo membro (anno).

📌 Esempio con Array di Caratteri

Quando la struttura contiene array di caratteri (stringhe), possiamo inizializzarli direttamente:

struct Libro {
    char titolo[100];
    char autore[50];
    int anno_pubblicazione;
    float prezzo;
};

int main() {
    struct Libro libro1 = {"Il Nome della Rosa", "Umberto Eco", 1980, 12.50};
    
    // La struttura libro1 ora contiene:
    // titolo = "Il Nome della Rosa"
    // autore = "Umberto Eco"
    // anno_pubblicazione = 1980
    // prezzo = 12.50
    
    return 0;
}

Le stringhe vengono copiate negli array di caratteri corrispondenti. È importante che le stringhe fornite non superino la dimensione degli array dichiarati nella struttura, altrimenti si verificherà un overflow di buffer.

🔹 Inizializzazione Parziale

Se forniamo meno valori di quanti siano i membri della struttura, i membri rimanenti vengono inizializzati automaticamente a zero (per i tipi numerici) o con stringhe vuote (per gli array di caratteri):

📌 Esempio di Inizializzazione Parziale

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    float media;
    int anno_iscrizione;
};

int main() {
    struct Studente studente1 = {"Mario", "Rossi"};
    
    // Risultato:
    // nome = "Mario"
    // cognome = "Rossi"
    // matricola = 0
    // media = 0.0
    // anno_iscrizione = 0
    
    return 0;
}

In questo caso, abbiamo fornito solo i primi due valori. Gli altri membri vengono automaticamente inizializzati a zero. Questo è particolarmente utile quando vogliamo azzerare completamente una struttura:

struct Studente studente2 = {0};  // Azzera tutti i membri
✓ Best Practice: Inizializzazione Completa

È sempre consigliabile inizializzare esplicitamente tutti i membri di una struttura, anche se alcuni devono essere impostati a zero. Questo rende il codice più leggibile e previene errori dovuti a valori non inizializzati. Inoltre, in caso di modifiche future alla struttura (aggiunta di nuovi membri), un'inizializzazione esplicita aiuta a identificare subito dove sono necessari aggiornamenti.

🔑 Accesso ai Membri di una Struttura

Una volta definita una variabile struttura, possiamo accedere ai suoi membri individuali utilizzando l'operatore punto (.), chiamato anche operatore di selezione diretta. La sintassi è:

nome_variabile.nome_membro

L'operatore punto crea un'espressione che si comporta esattamente come una normale variabile del tipo del membro. Possiamo quindi leggerla, modificarla, utilizzarla in espressioni, passarla a funzioni, e così via.

📌 Esempio Completo di Accesso e Modifica

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    float media;
    int anno_iscrizione;
};

int main() {
    struct Studente studente1;
    
    // Assegnazione dei valori ai membri
    strcpy(studente1.nome, "Mario");
    strcpy(studente1.cognome, "Rossi");
    studente1.matricola = 123456;
    studente1.media = 27.5;
    studente1.anno_iscrizione = 2020;
    
    // Lettura e stampa dei valori
    printf("Nome: %s\n", studente1.nome);
    printf("Cognome: %s\n", studente1.cognome);
    printf("Matricola: %d\n", studente1.matricola);
    printf("Media: %.2f\n", studente1.media);
    printf("Anno di iscrizione: %d\n", studente1.anno_iscrizione);
    
    // Modifica di un valore
    studente1.media = 28.0;
    printf("Nuova media: %.2f\n", studente1.media);
    
    return 0;
}

In questo esempio, utilizziamo strcpy() per copiare le stringhe negli array di caratteri della struttura. Per i tipi numerici (matricola, media, anno_iscrizione) possiamo utilizzare direttamente l'operatore di assegnazione =. Ogni membro può essere trattato come una normale variabile del suo tipo.

💡 Perché Usare strcpy() per le Stringhe?

Non possiamo assegnare direttamente una stringa a un array con =. L'istruzione studente1.nome = "Mario"; genererebbe un errore di compilazione. Questo perché studente1.nome è un array, e gli array in C non possono essere riassegnati dopo la loro creazione. Dobbiamo utilizzare funzioni come strcpy() o strncpy() (più sicura) per copiare i caratteri nell'array. Per utilizzare queste funzioni, è necessario includere l'header <string.h>.

📌 Utilizzo dei Membri in Espressioni

I membri di una struttura possono essere utilizzati in qualsiasi espressione come normali variabili:

👁️ Visualizza codice completo
#include <stdio.h>

struct Punto2D {
    float x;
    float y;
};

int main() {
    struct Punto2D p1 = {3.0, 4.0};
    struct Punto2D p2 = {6.0, 8.0};
    
    // Calcolo della distanza dall'origine per p1
    float distanza1 = sqrt(p1.x * p1.x + p1.y * p1.y);
    printf("Distanza di p1 dall'origine: %.2f\n", distanza1);
    
    // Calcolo della distanza tra i due punti
    float dx = p2.x - p1.x;
    float dy = p2.y - p1.y;
    float distanza = sqrt(dx * dx + dy * dy);
    printf("Distanza tra p1 e p2: %.2f\n", distanza);
    
    // Modifica delle coordinate
    p1.x += 1.0;
    p1.y += 1.0;
    printf("Nuovo p1: (%.2f, %.2f)\n", p1.x, p1.y);
    
    return 0;
}

I membri x e y vengono utilizzati esattamente come normali variabili float in calcoli matematici, confronti e operazioni di modifica.

⚠️ Attenzione alla Lettura di Input

Quando leggiamo stringhe da input standard in membri struttura che sono array di caratteri, dobbiamo fare attenzione. Per scanf() con il formato %s, NON si usa l'operatore & perché il nome dell'array è già un puntatore:

scanf("%s", studente1.nome);  // CORRETTO
scanf("%s", &studente1.nome); // ERRATO

Per i membri di tipo numerico, invece, l'operatore & è necessario:

scanf("%d", &studente1.matricola);  // CORRETTO
scanf("%d", studente1.matricola);   // ERRATO

È preferibile utilizzare fgets() per leggere stringhe in modo più sicuro, specificando la dimensione massima dell'array:

fgets(studente1.nome, sizeof(studente1.nome), stdin);

📋 Copia e Assegnazione di Strutture

Una delle caratteristiche molto comode delle strutture in C è la possibilità di copiare un'intera struttura in un'altra utilizzando semplicemente l'operatore di assegnazione =. Questa operazione esegue una copia membro per membro, copiando ogni singolo campo dalla struttura sorgente alla struttura destinazione.

🔄 Assegnazione Diretta tra Strutture

Se abbiamo due variabili dello stesso tipo struttura, possiamo copiarne una nell'altra con una singola istruzione di assegnazione:

struct_var1 = struct_var2;  // Copia tutti i membri da var2 a var1

📌 Esempio di Copia di Strutture

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

struct Libro {
    char titolo[100];
    char autore[50];
    int anno_pubblicazione;
    float prezzo;
};

int main() {
    struct Libro libro1;
    struct Libro libro2;
    
    // Inizializziamo libro1
    strcpy(libro1.titolo, "1984");
    strcpy(libro1.autore, "George Orwell");
    libro1.anno_pubblicazione = 1949;
    libro1.prezzo = 15.99;
    
    // Copiamo libro1 in libro2 con una singola istruzione
    libro2 = libro1;
    
    // Stampa per verificare la copia
    printf("Libro 2:\n");
    printf("Titolo: %s\n", libro2.titolo);
    printf("Autore: %s\n", libro2.autore);
    printf("Anno: %d\n", libro2.anno_pubblicazione);
    printf("Prezzo: %.2f\n", libro2.prezzo);
    
    // Modifichiamo libro2 senza influenzare libro1
    strcpy(libro2.titolo, "La Fattoria degli Animali");
    libro2.anno_pubblicazione = 1945;
    
    printf("\nDopo la modifica:\n");
    printf("Libro 1: %s (%d)\n", libro1.titolo, libro1.anno_pubblicazione);
    printf("Libro 2: %s (%d)\n", libro2.titolo, libro2.anno_pubblicazione);
    
    return 0;
}

L'istruzione libro2 = libro1; copia tutti i membri di libro1 in libro2. Questo include anche gli array di caratteri: vengono copiati tutti i caratteri di libro1.titolo in libro2.titolo, e così via. Le due strutture sono completamente indipendenti: modificare libro2 non influenza libro1 e viceversa.

✓ Vantaggi dell'Assegnazione Diretta

La possibilità di copiare strutture con una singola istruzione è estremamente comoda e rende il codice molto più leggibile rispetto alla copia manuale di ogni membro. Senza questa funzionalità, dovremmo scrivere:

strcpy(libro2.titolo, libro1.titolo);
strcpy(libro2.autore, libro1.autore);
libro2.anno_pubblicazione = libro1.anno_pubblicazione;
libro2.prezzo = libro1.prezzo;

Con l'assegnazione diretta, tutto questo si riduce a: libro2 = libro1;

🆚 Impossibilità del Confronto Diretto

È importante notare che, mentre possiamo assegnare una struttura a un'altra con =, NON possiamo confrontare due strutture direttamente con gli operatori == o !=. Il seguente codice genererà un errore di compilazione:

struct Punto2D p1 = {3.0, 4.0};
struct Punto2D p2 = {3.0, 4.0};

if (p1 == p2) {  // ERRORE! Non possiamo confrontare strutture direttamente
    printf("I punti sono uguali\n");
}

Per confrontare strutture, dobbiamo confrontare manualmente ogni membro:

struct Punto2D p1 = {3.0, 4.0};
struct Punto2D p2 = {3.0, 4.0};

if (p1.x == p2.x && p1.y == p2.y) {  // CORRETTO
    printf("I punti sono uguali\n");
}

📌 Funzione per Confrontare Strutture

È buona pratica creare funzioni dedicate per confrontare strutture complesse:

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

struct Data {
    int giorno;
    int mese;
    int anno;
};

// Funzione che restituisce 1 se le date sono uguali, 0 altrimenti
int date_uguali(struct Data d1, struct Data d2) {
    return (d1.giorno == d2.giorno && 
            d1.mese == d2.mese && 
            d1.anno == d2.anno);
}

// Funzione che restituisce 1 se d1 è precedente a d2, 0 altrimenti
int data_precedente(struct Data d1, struct Data d2) {
    if (d1.anno < d2.anno) return 1;
    if (d1.anno > d2.anno) return 0;
    if (d1.mese < d2.mese) return 1;
    if (d1.mese > d2.mese) return 0;
    if (d1.giorno < d2.giorno) return 1;
    return 0;
}

int main() {
    struct Data nascita = {15, 3, 2000};
    struct Data oggi = {31, 10, 2025};
    
    if (date_uguali(nascita, oggi)) {
        printf("Le date sono uguali\n");
    } else {
        printf("Le date sono diverse\n");
    }
    
    if (data_precedente(nascita, oggi)) {
        printf("La data di nascita precede oggi\n");
    }
    
    return 0;
}

Creare funzioni specifiche per confrontare strutture rende il codice più modulare, riutilizzabile e leggibile. Inoltre, possiamo implementare logiche di confronto complesse (come il confronto tra date) in modo centralizzato.

📊 Array di Strutture

Una delle applicazioni più potenti delle strutture è la loro combinazione con gli array. Possiamo creare array di strutture esattamente come creiamo array di tipi base (int, float, ecc.). Questo ci permette di gestire collezioni di entità complesse in modo organizzato ed efficiente. Un array di strutture è una collezione di elementi, dove ogni elemento è una struttura completa con tutti i suoi membri.

🔧 Dichiarazione e Inizializzazione

La sintassi per dichiarare un array di strutture è simile a quella di un normale array:

struct nome_struttura nome_array[dimensione];

📌 Esempio: Array di Studenti

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    float media;
};

int main() {
    struct Studente classe[3];  // Array di 3 studenti
    
    // Inizializzazione del primo studente
    strcpy(classe[0].nome, "Mario");
    strcpy(classe[0].cognome, "Rossi");
    classe[0].matricola = 12345;
    classe[0].media = 27.5;
    
    // Inizializzazione del secondo studente
    strcpy(classe[1].nome, "Laura");
    strcpy(classe[1].cognome, "Bianchi");
    classe[1].matricola = 12346;
    classe[1].media = 28.3;
    
    // Inizializzazione del terzo studente
    strcpy(classe[2].nome, "Giuseppe");
    strcpy(classe[2].cognome, "Verdi");
    classe[2].matricola = 12347;
    classe[2].media = 26.8;
    
    // Stampa di tutti gli studenti
    for (int i = 0; i < 3; i++) {
        printf("Studente %d:\n", i + 1);
        printf("  Nome: %s %s\n", classe[i].nome, classe[i].cognome);
        printf("  Matricola: %d\n", classe[i].matricola);
        printf("  Media: %.2f\n\n", classe[i].media);
    }
    
    return 0;
}

In questo esempio, classe è un array che contiene 3 elementi, ciascuno dei quali è una struttura Studente completa. Per accedere ai membri di ciascuna struttura, utilizziamo la notazione array[indice].membro. Prima specifichiamo quale elemento dell'array vogliamo (classe[0]), poi quale membro di quella struttura (.nome).

🔢 Inizializzazione Diretta di Array di Strutture

Possiamo inizializzare un array di strutture al momento della dichiarazione utilizzando liste annidate di valori:

📌 Esempio di Inizializzazione Diretta

#include <stdio.h>

struct Punto2D {
    float x;
    float y;
};

int main() {
    // Array di 4 punti inizializzato direttamente
    struct Punto2D punti[4] = {
        {0.0, 0.0},    // punti[0]
        {1.0, 1.0},    // punti[1]
        {2.0, 4.0},    // punti[2]
        {3.0, 9.0}     // punti[3]
    };
    
    // Stampa delle coordinate
    for (int i = 0; i < 4; i++) {
        printf("Punto %d: (%.2f, %.2f)\n", i, punti[i].x, punti[i].y);
    }
    
    return 0;
}

Ogni coppia di parentesi graffe interne rappresenta l'inizializzazione di un elemento dell'array (una struttura completa). I valori all'interno di ciascuna coppia di graffe vengono assegnati ai membri della struttura nell'ordine di dichiarazione.

📈 Operazioni su Array di Strutture

Gli array di strutture sono particolarmente utili per elaborazioni di dati che richiedono iterazioni su collezioni di entità complesse. Possiamo eseguire operazioni come ricerca, ordinamento, calcolo di statistiche, e altro ancora.

📌 Esempio Completo: Gestione Voti di una Classe

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

#define MAX_STUDENTI 5

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    float media;
};

// Funzione per calcolare la media della classe
float media_classe(struct Studente studenti[], int n) {
    float somma = 0.0;
    for (int i = 0; i < n; i++) {
        somma += studenti[i].media;
    }
    return somma / n;
}

// Funzione per trovare lo studente con la media più alta
int trova_miglior_studente(struct Studente studenti[], int n) {
    int indice_migliore = 0;
    for (int i = 1; i < n; i++) {
        if (studenti[i].media > studenti[indice_migliore].media) {
            indice_migliore = i;
        }
    }
    return indice_migliore;
}

// Funzione per stampare tutti gli studenti
void stampa_studenti(struct Studente studenti[], int n) {
    printf("\nElenco completo studenti:\n");
    printf("%-20s %-20s %-10s %-10s\n", "Nome", "Cognome", "Matricola", "Media");
    printf("----------------------------------------------------------------\n");
    for (int i = 0; i < n; i++) {
        printf("%-20s %-20s %-10d %-10.2f\n", 
               studenti[i].nome, 
               studenti[i].cognome, 
               studenti[i].matricola, 
               studenti[i].media);
    }
}

int main() {
    struct Studente classe[MAX_STUDENTI] = {
        {"Mario", "Rossi", 12345, 27.5},
        {"Laura", "Bianchi", 12346, 28.3},
        {"Giuseppe", "Verdi", 12347, 26.8},
        {"Anna", "Neri", 12348, 29.1},
        {"Paolo", "Gialli", 12349, 25.9}
    };
    
    // Stampa tutti gli studenti
    stampa_studenti(classe, MAX_STUDENTI);
    
    // Calcola la media della classe
    float media = media_classe(classe, MAX_STUDENTI);
    printf("\nMedia della classe: %.2f\n", media);
    
    // Trova lo studente migliore
    int indice_migliore = trova_miglior_studente(classe, MAX_STUDENTI);
    printf("\nStudente con la media più alta:\n");
    printf("%s %s - Media: %.2f\n", 
           classe[indice_migliore].nome, 
           classe[indice_migliore].cognome, 
           classe[indice_migliore].media);
    
    // Conta quanti studenti hanno media >= 28
    int count = 0;
    for (int i = 0; i < MAX_STUDENTI; i++) {
        if (classe[i].media >= 28.0) {
            count++;
        }
    }
    printf("\nStudenti con media >= 28: %d\n", count);
    
    return 0;
}

Questo esempio dimostra come gli array di strutture possano essere utilizzati per gestire dati complessi in modo organizzato. Possiamo passare l'intero array a funzioni, iterare su di esso per eseguire calcoli, cercare elementi specifici, e molto altro. L'uso di #define MAX_STUDENTI 5 rende il codice facilmente scalabile: se in futuro dovessimo gestire più studenti, basterebbe modificare questo valore.

⚠️ Dimensione degli Array di Strutture

Gli array di strutture possono occupare molta memoria. Se ogni struttura è grande (ad esempio contiene molti array di caratteri o altri array), un array di molte strutture può rapidamente consumare la memoria disponibile. Ad esempio, se una struttura Studente occupa 116 byte (50 + 50 + 4 + 4 + 4 + padding), un array di 1000 studenti occuperebbe circa 116 KB di memoria. È importante considerare questi aspetti quando si progettano applicazioni che gestiscono grandi quantità di dati. In questi casi, per gestire strutture molto grandi o in grande numero, si utilizzano tecniche avanzate con allocazione dinamica della memoria (malloc/free), che saranno trattate nella lezione dedicata ai puntatori.

🏢 Strutture Annidate

Una struttura può contenere come membri altre strutture. Questo concetto, chiamato annidamento di strutture o strutture annidate, permette di creare gerarchie di dati molto complesse e di rappresentare relazioni più elaborate tra le entità. L'annidamento riflette la natura gerarchica di molte entità del mondo reale.

🔗 Concetto di Annidamento

Quando una struttura contiene un'altra struttura come membro, possiamo accedere ai membri della struttura interna concatenando l'operatore punto. Se abbiamo struct A { struct B b; } e struct A a;, possiamo accedere ai membri di b con a.b.membro.

📌 Esempio Base: Indirizzo in una Persona

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

struct Indirizzo {
    char via[100];
    int numero_civico;
    char citta[50];
    char cap[10];
};

struct Persona {
    char nome[50];
    char cognome[50];
    int eta;
    struct Indirizzo residenza;  // Struttura annidata
};

int main() {
    struct Persona persona1;
    
    // Assegnazione dati personali
    strcpy(persona1.nome, "Mario");
    strcpy(persona1.cognome, "Rossi");
    persona1.eta = 35;
    
    // Assegnazione dati indirizzo (struttura annidata)
    strcpy(persona1.residenza.via, "Via Roma");
    persona1.residenza.numero_civico = 42;
    strcpy(persona1.residenza.citta, "Milano");
    strcpy(persona1.residenza.cap, "20100");
    
    // Stampa completa
    printf("Dati persona:\n");
    printf("Nome: %s %s\n", persona1.nome, persona1.cognome);
    printf("Età: %d anni\n", persona1.eta);
    printf("Indirizzo: %s %d, %s %s\n", 
           persona1.residenza.via,
           persona1.residenza.numero_civico,
           persona1.residenza.citta,
           persona1.residenza.cap);
    
    return 0;
}

In questo esempio, la struttura Persona contiene una struttura Indirizzo come membro. Per accedere alla via della residenza, utilizziamo persona1.residenza.via, concatenando due operatori punto. Questo permette di organizzare i dati in modo logico e gerarchico: i dati dell'indirizzo sono raggruppati insieme all'interno della persona, riflettendo la relazione semantica tra questi concetti.

📚 Inizializzazione di Strutture Annidate

Le strutture annidate possono essere inizializzate durante la dichiarazione utilizzando parentesi graffe annidate. Ogni livello di annidamento richiede il proprio set di parentesi graffe:

📌 Esempio di Inizializzazione Annidata

#include <stdio.h>

struct Data {
    int giorno;
    int mese;
    int anno;
};

struct Evento {
    char nome[100];
    struct Data data_evento;
    char luogo[100];
};

int main() {
    // Inizializzazione completa con strutture annidate
    struct Evento conferenza = {
        "Conferenza Internazionale di Informatica",
        {15, 6, 2026},  // Inizializzazione della struttura Data annidata
        "Centro Congressi Milano"
    };
    
    printf("Evento: %s\n", conferenza.nome);
    printf("Data: %02d/%02d/%d\n", 
           conferenza.data_evento.giorno,
           conferenza.data_evento.mese,
           conferenza.data_evento.anno);
    printf("Luogo: %s\n", conferenza.luogo);
    
    return 0;
}

Le parentesi graffe interne {15, 6, 2026} inizializzano la struttura Data annidata all'interno di Evento. Questa sintassi mantiene la struttura gerarchica dei dati anche durante l'inizializzazione.

🌐 Annidamenti Multipli

Possiamo avere livelli multipli di annidamento, creando gerarchie di dati complesse. Tuttavia, è importante mantenere un equilibrio: troppi livelli di annidamento possono rendere il codice difficile da leggere e mantenere.

📌 Esempio Complesso: Sistema di Gestione Universitaria

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

struct Data {
    int giorno;
    int mese;
    int anno;
};

struct Indirizzo {
    char via[100];
    int numero_civico;
    char citta[50];
    char cap[10];
};

struct Studente {
    char nome[50];
    char cognome[50];
    int matricola;
    struct Data data_nascita;
    struct Indirizzo residenza;
    char corso_laurea[100];
    float media;
};

int main() {
    struct Studente studente = {
        "Mario",
        "Rossi",
        123456,
        {15, 3, 2000},                    // Data di nascita
        {"Via Roma", 42, "Milano", "20100"}, // Indirizzo
        "Ingegneria Informatica",
        27.5
    };
    
    // Stampa completa dello studente
    printf("=== SCHEDA STUDENTE ===\n\n");
    printf("Dati Anagrafici:\n");
    printf("  Nome: %s %s\n", studente.nome, studente.cognome);
    printf("  Matricola: %d\n", studente.matricola);
    printf("  Data di nascita: %02d/%02d/%d\n", 
           studente.data_nascita.giorno,
           studente.data_nascita.mese,
           studente.data_nascita.anno);
    
    printf("\nResidenza:\n");
    printf("  %s %d\n", studente.residenza.via, studente.residenza.numero_civico);
    printf("  %s - CAP %s\n", studente.residenza.citta, studente.residenza.cap);
    
    printf("\nDati Accademici:\n");
    printf("  Corso di Laurea: %s\n", studente.corso_laurea);
    printf("  Media: %.2f\n", studente.media);
    
    return 0;
}

Questo esempio mostra come strutture annidate multiple permettano di creare rappresentazioni dettagliate e organizzate di entità complesse. La struttura Studente contiene due strutture annidate (Data e Indirizzo), ciascuna delle quali incapsula un gruppo logico di informazioni correlate.

✓ Vantaggi delle Strutture Annidate
  • Organizzazione Logica: I dati vengono raggruppati seguendo la loro relazione semantica naturale
  • Riutilizzabilità: Strutture come Data e Indirizzo possono essere riutilizzate in diverse altre strutture
  • Manutenibilità: Modifiche a una struttura base (es. aggiungere un campo "provincia" a Indirizzo) si propagano automaticamente a tutte le strutture che la utilizzano
  • Leggibilità: Il codice riflette chiaramente la struttura gerarchica dei dati

🏷️ L'Uso di typedef con le Strutture

La parola chiave typedef in C permette di creare alias (nomi alternativi) per tipi di dato esistenti. Quando utilizzata con le strutture, typedef ci consente di semplificare notevolmente la sintassi, eliminando la necessità di scrivere ripetutamente la parola chiave struct ogni volta che dichiariamo una variabile di quel tipo.

📋 Sintassi di typedef per Strutture

Esistono due modi principali per utilizzare typedef con le strutture. Il primo metodo dichiara la struttura e crea l'alias in un unico passaggio:

typedef struct {
    tipo1 membro1;
    tipo2 membro2;
    // ...
} NomeAlias;

Oppure possiamo mantenere il tag della struttura e creare comunque un alias:

typedef struct nome_tag {
    tipo1 membro1;
    tipo2 membro2;
    // ...
} NomeAlias;

📌 Confronto: Con e Senza typedef

Senza typedef:

struct Punto2D {
    float x;
    float y;
};

int main() {
    struct Punto2D p1;           // Dobbiamo scrivere "struct" ogni volta
    struct Punto2D p2 = {3.0, 4.0};
    struct Punto2D punti[10];
    return 0;
}

Con typedef:

typedef struct {
    float x;
    float y;
} Punto2D;

int main() {
    Punto2D p1;              // Sintassi più semplice e pulita
    Punto2D p2 = {3.0, 4.0};
    Punto2D punti[10];
    return 0;
}

Come si può vedere, l'uso di typedef rende il codice significativamente più leggibile e conciso. Punto2D diventa un vero e proprio tipo di dato, utilizzabile come int o float, senza dover specificare struct ogni volta.

💼 Esempi Pratici con typedef

📌 Sistema di Gestione Biblioteca

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

typedef struct {
    int giorno;
    int mese;
    int anno;
} Data;

typedef struct {
    char titolo[100];
    char autore[50];
    char isbn[20];
    Data data_pubblicazione;
    float prezzo;
    int disponibile;  // 1 = disponibile, 0 = prestato
} Libro;

typedef struct {
    char nome[50];
    char cognome[50];
    int id_utente;
    Data data_iscrizione;
} Utente;

// Funzione per stampare i dati di un libro
void stampa_libro(Libro l) {
    printf("=== DATI LIBRO ===\n");
    printf("Titolo: %s\n", l.titolo);
    printf("Autore: %s\n", l.autore);
    printf("ISBN: %s\n", l.isbn);
    printf("Data pubblicazione: %02d/%02d/%d\n", 
           l.data_pubblicazione.giorno,
           l.data_pubblicazione.mese,
           l.data_pubblicazione.anno);
    printf("Prezzo: %.2f€\n", l.prezzo);
    printf("Stato: %s\n\n", l.disponibile ? "Disponibile" : "Prestato");
}

// Funzione per stampare i dati di un utente
void stampa_utente(Utente u) {
    printf("=== DATI UTENTE ===\n");
    printf("Nome: %s %s\n", u.nome, u.cognome);
    printf("ID: %d\n", u.id_utente);
    printf("Iscritto dal: %02d/%02d/%d\n\n",
           u.data_iscrizione.giorno,
           u.data_iscrizione.mese,
           u.data_iscrizione.anno);
}

int main() {
    // Creazione di alcuni libri
    Libro catalogo[3] = {
        {"Il Nome della Rosa", "Umberto Eco", "978-0156001311", 
         {1, 1, 1980}, 15.99, 1},
        {"1984", "George Orwell", "978-0451524935", 
         {8, 6, 1949}, 12.50, 0},
        {"Il Signore degli Anelli", "J.R.R. Tolkien", "978-0544003415", 
         {29, 7, 1954}, 25.00, 1}
    };
    
    // Creazione di un utente
    Utente utente1 = {"Mario", "Rossi", 1001, {15, 9, 2020}};
    
    // Stampa delle informazioni
    stampa_utente(utente1);
    
    printf("CATALOGO BIBLIOTECA:\n\n");
    for (int i = 0; i < 3; i++) {
        stampa_libro(catalogo[i]);
    }
    
    // Conta libri disponibili
    int disponibili = 0;
    for (int i = 0; i < 3; i++) {
        if (catalogo[i].disponibile) {
            disponibili++;
        }
    }
    printf("Libri disponibili: %d su 3\n", disponibili);
    
    return 0;
}

Questo esempio mostra come typedef renda il codice molto più pulito e professionale. Possiamo dichiarare variabili di tipo Libro, Utente e Data esattamente come faremmo con int o float. Inoltre, le funzioni che accettano questi tipi come parametri hanno firme molto più leggibili.

✓ Convenzioni di Nomenclatura

Quando si usa typedef con le strutture, è comune utilizzare la convenzione PascalCase (prima lettera maiuscola) per il nome dell'alias, per distinguerlo visivamente dalle variabili normali (che usano snake_case o camelCase). Ad esempio: Studente, Punto2D, ContoCorrente. Questa convenzione rende immediatamente chiaro che stiamo lavorando con un tipo di dato definito dall'utente.

💡 typedef: Pro e Contro

Vantaggi:

  • Sintassi più pulita e leggibile
  • Il tipo struttura si comporta come un tipo nativo
  • Riduce la verbosità del codice
  • Facilita le modifiche future al tipo

Svantaggi:

  • Può nascondere il fatto che si sta lavorando con una struttura
  • In progetti molto grandi, può creare confusione se non usato con criterio
  • Alcuni standard di codifica preferiscono l'uso esplicito di struct per maggiore chiarezza

🔄 Passaggio di Strutture alle Funzioni

Le strutture possono essere passate alle funzioni come argomenti e possono anche essere restituite dalle funzioni come valori di ritorno. In C, quando passiamo una struttura a una funzione, viene creata una copia completa della struttura (passaggio per valore). Questo significa che la funzione lavora su una copia indipendente dei dati, e qualsiasi modifica effettuata all'interno della funzione non influenza la struttura originale.

💡 Nota sui Puntatori

In questa lezione ci concentriamo sul passaggio di strutture per valore. Esiste anche la possibilità di passare strutture tramite puntatori, che è più efficiente per strutture grandi e permette di modificare la struttura originale. Tuttavia, i puntatori e il loro utilizzo con le strutture sono trattati in dettaglio nella lezione dedicata ai puntatori, dove viene anche spiegato l'uso dell'operatore -> per l'accesso ai membri attraverso puntatori.

📥 Passaggio per Valore

Quando passiamo una struttura per valore, viene creata una copia completa di tutti i suoi membri. La funzione riceve questa copia e può leggerla e modificarla, ma qualsiasi modifica rimarrà locale alla funzione e non influenzerà la struttura originale nel chiamante.

📌 Esempio: Stampa di una Struttura

👁️ Visualizza codice completo
#include <stdio.h>

typedef struct {
    char nome[50];
    char cognome[50];
    int eta;
    float altezza;
} Persona;

// Funzione che riceve una struttura per valore
void stampa_persona(Persona p) {
    printf("Nome: %s %s\n", p.nome, p.cognome);
    printf("Età: %d anni\n", p.eta);
    printf("Altezza: %.2f m\n", p.altezza);
}

// Funzione che tenta di modificare una struttura passata per valore
void compleanno(Persona p) {
    p.eta++;  // Questa modifica è locale alla funzione
    printf("Dentro compleanno: %s ora ha %d anni\n", p.nome, p.eta);
}

int main() {
    Persona persona1 = {"Mario", "Rossi", 30, 1.75};
    
    printf("=== Dati iniziali ===\n");
    stampa_persona(persona1);
    
    printf("\n=== Chiamata a compleanno ===\n");
    compleanno(persona1);
    
    printf("\n=== Dati dopo compleanno ===\n");
    stampa_persona(persona1);  // L'età è ancora 30!
    
    return 0;
}

L'output di questo programma mostrerà che, nonostante la funzione compleanno() incrementi l'età all'interno della funzione stessa, la struttura persona1 nel main() mantiene l'età originale di 30 anni. Questo perché compleanno() lavora su una copia della struttura, non sull'originale.

⚠️ Performance con Strutture Grandi

Il passaggio per valore copia l'intera struttura, inclusi tutti i suoi array e membri. Se la struttura è grande (ad esempio, contiene molti array di caratteri o array di numeri), questa copia può essere costosa in termini di tempo e memoria. Per strutture grandi, è preferibile utilizzare il passaggio tramite puntatori (trattato nella lezione sui puntatori), che passa solo l'indirizzo della struttura invece di copiare tutti i suoi dati.

📤 Restituzione di Strutture da Funzioni

Una funzione può restituire una struttura come valore di ritorno. Anche in questo caso, viene creata una copia della struttura che viene restituita al chiamante.

📌 Esempio: Funzioni che Restituiscono Strutture

👁️ Visualizza codice completo
#include <stdio.h>
#include <string.h>

typedef struct {
    float x;
    float y;
} Punto2D;

typedef struct {
    char nome[50];
    int eta;
    float stipendio;
} Dipendente;

// Funzione che restituisce un punto
Punto2D crea_punto(float x, float y) {
    Punto2D p;
    p.x = x;
    p.y = y;
    return p;
}

// Funzione che calcola la somma di due punti
Punto2D somma_punti(Punto2D p1, Punto2D p2) {
    Punto2D risultato;
    risultato.x = p1.x + p2.x;
    risultato.y = p1.y + p2.y;
    return risultato;
}

// Funzione che aumenta lo stipendio di un dipendente
Dipendente aumenta_stipendio(Dipendente d, float percentuale) {
    Dipendente nuovo = d;  // Copia del dipendente
    nuovo.stipendio = d.stipendio * (1 + percentuale / 100);
    return nuovo;
}

// Funzione che crea un dipendente
Dipendente crea_dipendente(char nome[], int eta, float stipendio) {
    Dipendente d;
    strcpy(d.nome, nome);
    d.eta = eta;
    d.stipendio = stipendio;
    return d;
}

int main() {
    // Esempio con punti
    Punto2D p1 = crea_punto(3.0, 4.0);
    Punto2D p2 = crea_punto(1.0, 2.0);
    Punto2D somma = somma_punti(p1, p2);
    
    printf("P1: (%.2f, %.2f)\n", p1.x, p1.y);
    printf("P2: (%.2f, %.2f)\n", p2.x, p2.y);
    printf("Somma: (%.2f, %.2f)\n\n", somma.x, somma.y);
    
    // Esempio con dipendenti
    Dipendente dip1 = crea_dipendente("Mario Rossi", 35, 2500.0);
    printf("Dipendente: %s\n", dip1.nome);
    printf("Stipendio iniziale: %.2f€\n", dip1.stipendio);
    
    Dipendente dip1_aumentato = aumenta_stipendio(dip1, 10);  // Aumento del 10%
    printf("Nuovo stipendio: %.2f€\n", dip1_aumentato.stipendio);
    printf("Stipendio originale (non modificato): %.2f€\n", dip1.stipendio);
    
    return 0;
}

Questo esempio mostra vari pattern comuni quando si lavora con funzioni che restituiscono strutture: funzioni "factory" che creano nuove istanze di strutture (crea_punto(), crea_dipendente()), funzioni che elaborano strutture e ne restituiscono di nuove (somma_punti()), e funzioni che creano versioni modificate di strutture esistenti (aumenta_stipendio()).

🎯 Funzioni Utility per Strutture

📌 Esempio Completo: Sistema di Gestione Coordinate

👁️ Visualizza codice completo
#include <stdio.h>
#include <math.h>

typedef struct {
    float x;
    float y;
} Punto2D;

// Crea un punto
Punto2D crea_punto(float x, float y) {
    Punto2D p = {x, y};
    return p;
}

// Somma due punti (vettorialmente)
Punto2D somma_punti(Punto2D p1, Punto2D p2) {
    return crea_punto(p1.x + p2.x, p1.y + p2.y);
}

// Sottrae due punti (vettorialmente)
Punto2D sottrai_punti(Punto2D p1, Punto2D p2) {
    return crea_punto(p1.x - p2.x, p1.y - p2.y);
}

// Moltiplica un punto per uno scalare
Punto2D moltiplica_scalare(Punto2D p, float scalare) {
    return crea_punto(p.x * scalare, p.y * scalare);
}

// Calcola la distanza tra due punti
float distanza(Punto2D p1, Punto2D p2) {
    float dx = p2.x - p1.x;
    float dy = p2.y - p1.y;
    return sqrt(dx * dx + dy * dy);
}

// Calcola la distanza dall'origine
float distanza_origine(Punto2D p) {
    return sqrt(p.x * p.x + p.y * p.y);
}

// Stampa un punto
void stampa_punto(Punto2D p) {
    printf("(%.2f, %.2f)", p.x, p.y);
}

int main() {
    Punto2D origine = crea_punto(0.0, 0.0);
    Punto2D p1 = crea_punto(3.0, 4.0);
    Punto2D p2 = crea_punto(6.0, 8.0);
    
    printf("Punto 1: ");
    stampa_punto(p1);
    printf("\nDistanza dall'origine: %.2f\n\n", distanza_origine(p1));
    
    printf("Punto 2: ");
    stampa_punto(p2);
    printf("\nDistanza dall'origine: %.2f\n\n", distanza_origine(p2));
    
    Punto2D somma = somma_punti(p1, p2);
    printf("Somma P1 + P2: ");
    stampa_punto(somma);
    printf("\n");
    
    Punto2D differenza = sottrai_punti(p2, p1);
    printf("Differenza P2 - P1: ");
    stampa_punto(differenza);
    printf("\n");
    
    Punto2D triplo = moltiplica_scalare(p1, 3.0);
    printf("P1 * 3: ");
    stampa_punto(triplo);
    printf("\n");
    
    float dist = distanza(p1, p2);
    printf("\nDistanza tra P1 e P2: %.2f\n", dist);
    
    return 0;
}

Questo esempio dimostra come creare una piccola libreria di funzioni utility per lavorare con una struttura. Ogni funzione ha uno scopo ben definito e può essere combinata con altre funzioni per eseguire operazioni più complesse. Questo approccio modulare rende il codice riutilizzabile e facile da mantenere.

✓ Best Practice per Funzioni con Strutture
  • Creare funzioni "factory" per inizializzare strutture in modo consistente
  • Creare funzioni utility per operazioni comuni (stampa, confronto, calcoli)
  • Restituire nuove strutture piuttosto che modificare quelle passate (approccio funzionale)
  • Usare nomi descrittivi per le funzioni che indicano chiaramente cosa fanno
  • Documentare bene le funzioni, specialmente se modificano i parametri

📏 Dimensione delle Strutture e Allineamento in Memoria

La dimensione di una struttura in memoria è un aspetto importante da comprendere, soprattutto quando si lavora con grandi quantità di dati o con sistemi embedded dove la memoria è limitata. Potremmo aspettarci che la dimensione di una struttura sia semplicemente la somma delle dimensioni di tutti i suoi membri, ma nella realtà non è sempre così a causa di un meccanismo chiamato padding o allineamento.

🔍 Il Concetto di Padding

Il compilatore C può inserire byte di "riempimento" (padding) tra i membri di una struttura per ottimizzare l'accesso alla memoria. Questo avviene perché i processori moderni accedono alla memoria in modo più efficiente quando i dati sono allineati a specifici confini di indirizzo. Ad esempio, un int (4 byte) viene tipicamente allineato a indirizzi multipli di 4, e un double (8 byte) a indirizzi multipli di 8.

📌 Esempio di Padding

#include <stdio.h>

struct Esempio1 {
    char c;      // 1 byte
    int i;       // 4 byte
    char d;      // 1 byte
};

struct Esempio2 {
    char c;      // 1 byte
    char d;      // 1 byte
    int i;       // 4 byte
};

int main() {
    printf("Dimensione di Esempio1: %zu byte\n", sizeof(struct Esempio1));
    printf("Dimensione di Esempio2: %zu byte\n", sizeof(struct Esempio2));
    
    printf("\nSomma delle dimensioni dei membri: 1 + 4 + 1 = 6 byte\n");
    printf("Ma la struttura potrebbe occupare 12 byte a causa del padding!\n");
    
    return 0;
}

Sorprendentemente, Esempio1 potrebbe occupare 12 byte invece dei 6 che ci aspetteremmo (1 + 4 + 1). Ecco perché:

  • char c occupa 1 byte all'offset 0
  • 3 byte di padding vengono inseriti per allineare il successivo int
  • int i occupa 4 byte all'offset 4 (multiplo di 4)
  • char d occupa 1 byte all'offset 8
  • 3 byte di padding vengono aggiunti alla fine per allineare la struttura

Esempio2, invece, potrebbe occupare solo 8 byte perché i due char sono consecutivi all'inizio, seguiti dall'int già allineato. Questo dimostra che l'ordine di dichiarazione dei membri influenza la dimensione totale della struttura.

📊 Uso di sizeof

L'operatore sizeof è fondamentale per determinare la dimensione effettiva di una struttura in memoria. Può essere applicato sia al tipo della struttura che a una variabile specifica:

📌 Esempio con sizeof

👁️ Visualizza codice completo
#include <stdio.h>

typedef struct {
    char nome[50];
    int eta;
    float altezza;
} Persona;

typedef struct {
    int giorno;
    int mese;
    int anno;
} Data;

int main() {
    Persona p;
    Data d;
    
    printf("Dimensione di Persona: %zu byte\n", sizeof(Persona));
    printf("Dimensione di Data: %zu byte\n", sizeof(Data));
    
    printf("\nDimensione della variabile p: %zu byte\n", sizeof(p));
    printf("Dimensione della variabile d: %zu byte\n", sizeof(d));
    
    printf("\nArray di 10 Persona occupa: %zu byte\n", sizeof(Persona) * 10);
    
    // Calcolo della dimensione dei singoli membri
    printf("\nDimensione dei membri di Persona:\n");
    printf("  nome[50]: %zu byte\n", sizeof(p.nome));
    printf("  eta: %zu byte\n", sizeof(p.eta));
    printf("  altezza: %zu byte\n", sizeof(p.altezza));
    printf("  Somma: %zu byte\n", 
           sizeof(p.nome) + sizeof(p.eta) + sizeof(p.altezza));
    
    return 0;
}

Questo esempio mostra come utilizzare sizeof per analizzare la dimensione di strutture e dei loro membri. La somma delle dimensioni dei membri individuali potrebbe essere inferiore alla dimensione totale della struttura a causa del padding.

💡 Ottimizzazione dell'Ordine dei Membri

Per minimizzare il padding e quindi la dimensione della struttura, è consigliabile dichiarare i membri in ordine decrescente di dimensione:

// Meno efficiente (più padding)
struct NonOttimizzata {
    char a;      // 1 byte + 3 padding
    int b;       // 4 byte
    char c;      // 1 byte + 3 padding
    double d;    // 8 byte
};  // Totale potrebbe essere 24 byte

// Più efficiente (meno padding)
struct Ottimizzata {
    double d;    // 8 byte
    int b;       // 4 byte
    char a;      // 1 byte
    char c;      // 1 byte + 2 padding
};  // Totale potrebbe essere 16 byte

Ordinando i membri dal più grande al più piccolo, riduciamo il padding necessario e quindi la dimensione totale della struttura.

⚠️ Portabilità e Padding

Il padding esatto e la dimensione delle strutture possono variare tra diverse architetture di processori, compilatori e sistemi operativi. Il codice che fa assunzioni specifiche sulla dimensione o layout delle strutture potrebbe non essere portabile. Utilizzare sempre sizeof per calcoli dinamici piuttosto che assumere dimensioni fisse.

🎓 Verifica le Tue Conoscenze

Domanda 1: Qual è la differenza principale tra un array e una struttura?

Mostra risposta

Un array è una collezione omogenea di elementi dello stesso tipo, accessibili tramite un indice numerico. Una struttura è una collezione eterogenea di elementi potenzialmente di tipo diverso, accessibili tramite nomi specifici. Gli array sono utili per gestire sequenze di dati simili, mentre le strutture sono ideali per raggruppare dati correlati ma di tipo diverso che descrivono un'entità unica.

Domanda 2: È possibile copiare una struttura in un'altra con l'operatore =? E confrontarle con ==?

Mostra risposta

, possiamo copiare una struttura in un'altra con l'operatore = (l'assegnazione esegue una copia membro per membro). No, non possiamo confrontare direttamente due strutture con == o !=. Per il confronto, dobbiamo confrontare manualmente ogni membro o creare funzioni apposite di confronto.

Domanda 3: Quando passiamo una struttura a una funzione per valore, cosa succede alla struttura originale se la funzione modifica i suoi membri?

Mostra risposta

La struttura originale non viene modificata. Quando passiamo una struttura per valore, viene creata una copia completa di tutti i membri. Qualsiasi modifica effettuata dalla funzione avviene solo sulla copia locale e non si riflette sulla struttura originale nel chiamante. Per modificare la struttura originale, sarebbe necessario utilizzare puntatori (trattati in una lezione dedicata).

Domanda 4: Qual è il vantaggio principale di usare typedef con le strutture?

Mostra risposta

Il vantaggio principale è la semplificazione della sintassi. Con typedef, possiamo dichiarare variabili di tipo struttura senza dover scrivere la parola chiave struct ogni volta. Ad esempio, invece di scrivere struct Studente s1;, possiamo semplicemente scrivere Studente s1;. Questo rende il codice più leggibile e il tipo struttura si comporta come un tipo nativo del linguaggio.

Domanda 5: Perché la dimensione di una struttura in memoria potrebbe essere maggiore della somma delle dimensioni dei suoi membri?

Mostra risposta

A causa del padding o allineamento in memoria. Il compilatore può inserire byte di riempimento tra i membri della struttura per allineare i dati a confini di indirizzo ottimali per il processore. Questo migliora le performance di accesso alla memoria ma aumenta la dimensione totale della struttura. L'ordine di dichiarazione dei membri può influenzare la quantità di padding necessario.

Domanda 6: Come si accede a un membro di una struttura annidata?

Mostra risposta

Si utilizza l'operatore punto . in modo concatenato. Ad esempio, se abbiamo una struttura Studente che contiene una struttura Data nel campo data_nascita, per accedere al giorno di nascita scriveremo: studente.data_nascita.giorno. Ogni operatore punto accede al livello successivo di annidamento della struttura.

Domanda 7: Come si inizializza un array di caratteri (stringa) all'interno di una struttura già definita?

Mostra risposta

Non possiamo usare l'operatore di assegnazione = direttamente. Dobbiamo utilizzare funzioni come strcpy() o strncpy() della libreria <string.h>. Ad esempio: strcpy(studente.nome, "Mario");. Questo perché il nome dell'array è un puntatore costante che non può essere riassegnato, ma possiamo copiare i caratteri nell'array esistente.

Domanda 8: È possibile creare un array di strutture? Se sì, come?

Mostra risposta

, è possibile e molto comune. La sintassi è: struct NomeStruttura nomeArray[dimensione]; oppure, se usiamo typedef: NomeStruttura nomeArray[dimensione];. Ogni elemento dell'array è una struttura completa. Per accedere ai membri: nomeArray[indice].membro. Gli array di strutture sono fondamentali per gestire collezioni di entità complesse, come elenchi di studenti, prodotti, coordinate, ecc.

Domanda 9: Qual è l'output del seguente codice?

struct Test {
    int a;
    int b;
};

int main() {
    struct Test t1 = {10, 20};
    struct Test t2 = t1;
    t2.a = 30;
    printf("%d %d\n", t1.a, t2.a);
    return 0;
}
Mostra risposta

L'output sarà: 10 30. L'istruzione t2 = t1 crea una copia completa di t1 in t2. Quando modifichiamo t2.a = 30, stiamo modificando solo la copia in t2, non l'originale t1. Quindi t1.a rimane 10 mentre t2.a diventa 30. Le due strutture sono completamente indipendenti dopo la copia.

Domanda 10: Quando dovremmo preferire l'uso di strutture annidate rispetto a una singola struttura "piatta"?

Mostra risposta

Le strutture annidate sono preferibili quando:

  • Esiste una chiara relazione gerarchica o di composizione tra i dati (es. una Persona ha un Indirizzo)
  • Vogliamo riutilizzare la stessa struttura secondaria in più contesti (es. Data può essere usata per date di nascita, scadenze, pubblicazioni, ecc.)
  • Vogliamo organizzare logicamente i dati correlati (raggruppare tutti i campi di un indirizzo insieme)
  • Vogliamo migliorare la manutenibilità: modifiche alla struttura annidata si propagano automaticamente

Una struttura piatta è preferibile quando i dati non hanno una chiara gerarchia e aggiungerla complicherebbe inutilmente il codice.

💻 Esercizi Pratici

Esercizio 1: Gestione di un Catalogo Prodotti

Creare un programma che gestisca un catalogo di prodotti. Ogni prodotto deve avere: codice (intero), nome (stringa), prezzo (float), e quantità in magazzino (intero).

Compiti:

  1. Definire la struttura Prodotto
  2. Creare un array di almeno 5 prodotti
  3. Implementare una funzione per stampare tutti i prodotti
  4. Implementare una funzione per calcolare il valore totale del magazzino
  5. Implementare una funzione per cercare un prodotto per codice
  6. Implementare una funzione per trovare il prodotto più costoso

Esercizio 2: Gestione di un Registro Studenti

Creare un programma per gestire un registro di studenti universitari.

Compiti:

  1. Definire una struttura Data con giorno, mese, anno
  2. Definire una struttura Studente con nome, cognome, matricola, data di nascita (struttura annidata), e media dei voti
  3. Creare un array di 10 studenti
  4. Implementare funzioni per:
    • Calcolare la media generale della classe
    • Trovare lo studente più giovane
    • Contare quanti studenti hanno media superiore a 27
    • Stampare l'elenco ordinato per media (opzionale, più difficile)

Esercizio 3: Sistema Geometrico

Creare un programma che lavori con forme geometriche bidimensionali.

Compiti:

  1. Definire una struttura Punto2D con coordinate x e y
  2. Definire una struttura Rettangolo con due punti (angolo superiore sinistro e angolo inferiore destro)
  3. Implementare funzioni per:
    • Calcolare l'area del rettangolo
    • Calcolare il perimetro del rettangolo
    • Verificare se un punto è interno al rettangolo
    • Verificare se due rettangoli si sovrappongono
  4. Creare un array di 5 rettangoli e trovare quello con l'area maggiore

Esercizio 4: Gestione Biblioteca (Avanzato)

Creare un sistema completo di gestione per una biblioteca.

Compiti:

  1. Definire strutture per:
    • Data: giorno, mese, anno
    • Libro: titolo, autore, ISBN, anno pubblicazione, disponibile (boolean)
    • Utente: nome, cognome, ID, data iscrizione
    • Prestito: ID utente, ISBN libro, data prestito, data restituzione prevista
  2. Implementare funzioni per:
    • Registrare un nuovo prestito
    • Registrare una restituzione
    • Elencare tutti i libri disponibili
    • Elencare tutti i prestiti attivi
    • Trovare tutti i libri di un determinato autore
    • Verificare se un libro è in ritardo nella restituzione

🎯 Conclusioni e Prospettive Future

Le strutture rappresentano uno degli strumenti più potenti e fondamentali del linguaggio C, permettendoci di creare tipi di dato complessi e personalizzati che rispecchiano le entità del mondo reale. In questa lezione abbiamo esplorato in profondità:

✓ Concetti Chiave Acquisiti
  • Dichiarazione e Definizione: Come creare nuovi tipi aggregati e istanziare variabili di questi tipi
  • Accesso ai Membri: Utilizzo dell'operatore punto per leggere e modificare i campi delle strutture
  • Operazioni su Strutture: Copia diretta con =, impossibilità di confronto diretto, inizializzazione
  • Array di Strutture: Gestione di collezioni di entità complesse in modo organizzato
  • Strutture Annidate: Creazione di gerarchie di dati per rappresentare relazioni complesse
  • typedef: Semplificazione della sintassi e creazione di alias per tipi struttura
  • Funzioni e Strutture: Passaggio per valore, restituzione di strutture, creazione di librerie di funzioni utility
  • Ottimizzazione Memoria: Comprensione del padding e tecniche per ridurre lo spreco di memoria
🔮 Prossimi Passi: I Puntatori

In questa lezione ci siamo concentrati sull'uso delle strutture con passaggio per valore. Questo approccio, pur essendo perfettamente valido e utile in molti contesti, presenta alcuni limiti:

  • Ogni passaggio a funzione copia l'intera struttura, il che può essere inefficiente per strutture grandi
  • Non possiamo modificare la struttura originale da dentro una funzione
  • Non possiamo creare strutture di dimensione dinamica a runtime

Nella lezione dedicata ai puntatori, approfondiremo:

  • Come passare strutture a funzioni tramite puntatori per evitare copie costose
  • L'operatore -> per accedere ai membri attraverso puntatori
  • L'allocazione dinamica di strutture con malloc() e free()
  • Strutture auto-referenziali per implementare liste concatenate, alberi e altre strutture dati complesse
  • Array dinamici di strutture
⚠️ Argomenti Non Trattati (Rimandati alla Lezione sui Puntatori)

Per mantenere questa lezione focalizzata sui concetti base delle strutture, abbiamo deliberatamente evitato di trattare:

  • Puntatori a strutture
  • Operatore ->
  • Allocazione dinamica con malloc(), calloc(), realloc()
  • Deallocazione con free()
  • Strutture auto-referenziali
  • Liste concatenate, pile, code implementate con strutture

Tutti questi argomenti avanzati richiedono una solida comprensione dei puntatori e sono trattati in dettaglio nella lezione apposita. Una volta compresi i puntatori, il potere espressivo delle strutture si moltiplica esponenzialmente, permettendo di implementare qualsiasi struttura dati immaginabile.

Le strutture sono un pilastro della programmazione in C e rappresentano il primo passo verso la programmazione orientata agli oggetti che troverete in linguaggi come C++, Java o Python. Padroneggiare le strutture significa acquisire la capacità di modellare il mondo reale nel codice, creando astrazioni potenti e manutenibili che rendono i programmi più chiari, organizzati e professionali.

Continuate a esercitarvi con gli esercizi proposti, sperimentate creando le vostre strutture per rappresentare entità che vi interessano, e preparatevi ad approfondire ulteriormente questi concetti quando studierete i puntatori. La combinazione di strutture e puntatori vi aprirà le porte a tecniche di programmazione estremamente avanzate ed efficaci.